/*
 * MIT License
 *
 * Copyright (c) 2025 milkpotatoes
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <string>
#include <unistd.h>
#include <uv.h>
#include <vector>

#include "js_native_api.h"
#include "js_native_api_types.h"
#include "js_utils/js_childprocess.h"
#include "runner_utils/log.h"
#include "runner_utils/napi_utils.h"
#include "runner_utils/utils.h"
#include "uv/unix.h"

JsChildProcess::JsChildProcess(napi_env env, napi_value onExit) : env_(env)
{
    napi_create_reference(env_, onExit, 1, &onExit_);
}

JsChildProcess::~JsChildProcess()
{
    if (onExit_ != nullptr)
    {
        napi_delete_reference(env_, onExit_);
        onExit_ = nullptr;
    }

    for (auto pipe = &in_; pipe <= &err_; ++pipe)
    {
        if (pipe->data.stream != nullptr && pipe->flags & UV_CREATE_PIPE)
        {
            if (!uv_is_closing(reinterpret_cast<uv_handle_t *>(pipe->data.stream)))
                uv_close(reinterpret_cast<uv_handle_t *>(pipe->data.stream),
                         [](uv_handle_t *handle) { delete reinterpret_cast<uv_pipe_t *>(handle); });
            else
                delete reinterpret_cast<uv_pipe_t *>(pipe->data.stream);
            pipe->data.stream = nullptr;
        }
    }
}

napi_value JsChildProcess::Init(napi_env env, napi_value exports)
{
#define DESC_NAPI_METHOD(name, callback) \
    {name, nullptr, callback, nullptr, nullptr, nullptr, napi_default_jsproperty, nullptr}

    std::vector<napi_property_descriptor> timerProp{
        DESC_NAPI_METHOD("_innerSpawn", InnerSpawn),
        DESC_NAPI_METHOD("_innerReadStream", InnerReadStream),
        DESC_NAPI_METHOD("_innerGetFd", InnerGetFd),
    };
#undef DESC_NAPI_METHOD

    napi_define_properties(env, exports, timerProp.size(), timerProp.data());
    return exports;
}

JsChildProcess *JsChildProcess::New(napi_env env, napi_value onExit)
{
    return new JsChildProcess(env, onExit);
}

void JsChildProcess::Delete()
{
    auto handle = reinterpret_cast<uv_handle_t *>(&subProcess_);
    if (!uv_is_closing(handle))
    {
        uv_close(handle, [](uv_handle_t *handle) {
            delete reinterpret_cast<JsChildProcess *>(reinterpret_cast<uv_process_t *>(handle)->data);
        });
        return;
    }
    delete this;
}

void JsChildProcess::Finalize([[maybe_unused]] napi_env env, void *data, [[maybe_unused]] void *hint)
{
    reinterpret_cast<JsChildProcess *>(data)->Delete();
}

void JsChildProcess::OnExit(int status, int signal)
{
    if (onExit_ == nullptr)
        return;
    [[maybe_unused]] utils::NapiHandleScope scope(env_);
    auto cb = utils::GetReferenceValue(env_, onExit_);
    napi_delete_reference(env_, onExit_);
    onExit_ = nullptr;
    std::vector<napi_value> args{
        utils::CreateInt32(env_, status),
        utils::CreateInt32(env_, signal),
    };
    napi_call_function(env_, utils::GetGlobal(env_), cb, args.size(), args.data(), nullptr);
}

void JsChildProcess::ExitCallback(uv_process_t *process, long status, int signal)
{
    reinterpret_cast<JsChildProcess *>(process->data)->OnExit(status, signal);
}

DEF_NAPI_CALLBACK(JsChildProcess::InnerSpawn, info)
{
    auto env = info.GetEnv();
    auto *subProcess = New(env, info[2]);
    OptionsParser options(env, info[1]);
    auto stdios = utils::ToVector(env, utils::GetElement(env, info[1], 5));
    InitStdio(env, stdios[0], &subProcess->in_, UV_READABLE_PIPE, STDIN_FILENO);
    InitStdio(env, stdios[1], &subProcess->out_, UV_WRITABLE_PIPE, STDOUT_FILENO);
    InitStdio(env, stdios[2], &subProcess->err_, UV_WRITABLE_PIPE, STDERR_FILENO);
    utils::Wrap(env, info[0], subProcess, Finalize);
    options->stdio_count = 3;
    options->stdio = &subProcess->in_;
    options->exit_cb = ExitCallback;
    subProcess->subProcess_.data = subProcess;
    int status{};
    if (status = uv_spawn(utils::GetUVLoop(info.GetEnv()), &subProcess->subProcess_, options); status != 0)
    {
        utils::RemoveWrap(env, info[0]);
        subProcess->Delete();
    }
    status;
    std::vector<uint64_t> results{
        reinterpret_cast<uint64_t>(subProcess->in_.data.stream),
        reinterpret_cast<uint64_t>(subProcess->out_.data.stream),
        reinterpret_cast<uint64_t>(subProcess->err_.data.stream),
    };
    std::vector<napi_value> resultsNapi{
        utils::CreateInt64(env, status),
        utils::ToArray(env, results.begin(), results.end(), utils::CreateBigIntUint64),
    };

    return utils::ToArray(env, resultsNapi.begin(), resultsNapi.end(),
                          [](napi_env env, napi_value value) { return value; });
}

DEF_NAPI_CALLBACK(JsChildProcess::InnerReadStream, info)
{
    bool lossless;
    uv_stream_t *stream{};
    int64_t fd = utils::GetValueBigIntInt64(info.GetEnv(), info[0], lossless);
    if (fd < 0)
    {
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "negative fd");
        return nullptr;
    }
    stream = reinterpret_cast<uv_stream_t *>(utils::GetValueBigIntUint64(info.GetEnv(), info[0], lossless));
    if (!lossless)
    {
        DEFAULT_LOGGER(ERROR) << "Failed to get fd lossless, fd: " << fd;
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "fd lossless");
        return nullptr;
    }

    if (uv_is_readable(stream) == 0)
    {
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "stream is not readable");
        return nullptr;
    }
    if (stream->data != nullptr)
    {
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "stream is already reading");
        return nullptr;
    }
    auto readStreamData = new ReadStreamData{info.GetEnv(), utils::CreateReference(info.GetEnv(), info[1])};
    stream->data = readStreamData;
    auto status = uv_read_start(
        stream,
        [](uv_handle_t *handle, size_t size, uv_buf_t *buf) {
            buf->len = size;
            buf->base = new char[size];
        },
        [](uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
            auto data = reinterpret_cast<ReadStreamData *>(stream->data);
            utils::NapiHandleScope scope(data->env);
            napi_value callback = utils::GetReferenceValue(data->env, data->callback);
            if (nread < 0)
            {
                napi_delete_reference(data->env, data->callback);
                delete data;
                stream->data = nullptr;
                uv_read_stop(stream);
            }
            else
            {
                void *result;
                auto buffer = utils::CreateArrayBuffer(data->env, nread, result);
                memcpy(result, buf->base, nread);
                std::vector<napi_value> args{buffer};
                napi_call_function(data->env, utils::GetGlobal(data->env), callback, args.size(), args.data(), nullptr);
            }
            delete[] buf->base;
        });

    if (status != 0)
    {
        napi_delete_reference(info.GetEnv(), readStreamData->callback);
        delete readStreamData;
        stream->data = nullptr;
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", uv_strerror(status));
        return nullptr;
    }

    return nullptr;
}

DEF_NAPI_CALLBACK(JsChildProcess::InnerGetFd, info)
{
    bool lossless;
    uv_stream_t *stream{};
    int64_t fd = utils::GetValueBigIntInt64(info.GetEnv(), info[0], lossless);
    if (fd < 0)
    {
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "negative fd");
        return nullptr;
    }
    stream = reinterpret_cast<uv_stream_t *>(utils::GetValueBigIntUint64(info.GetEnv(), info[0], lossless));
    if (!lossless)
    {
        DEFAULT_LOGGER(ERROR) << "Failed to get fd lossless, fd: " << fd;
        napi_throw_error(info.GetEnv(), "Failed to execute InnerReadStream", "fd lossless");
        return nullptr;
    }
    uv_os_fd_t osFd;
    uv_fileno(reinterpret_cast<uv_handle_t *>(stream), &osFd);
    return utils::CreateInt64(info.GetEnv(), osFd);
}

void JsChildProcess::InitStdio(napi_env env, napi_value option, uv_stdio_container_t *stdio, uv_stdio_flags direction,
                               int defaultFd)
{
    auto type = utils::TypeOf(env, option);
    stdio->data.stream = nullptr;
    if (type == napi_string)
    {
        auto value = utils::GetValueStringUtf8(env, option);
        if (value == "pipe")
        {
            stdio->flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | direction);
            auto pipe = new uv_pipe_t{};
            uv_pipe_init(utils::GetUVLoop(env), pipe, 0);
            stdio->data.stream = reinterpret_cast<uv_stream_t *>(pipe);
            return;
        }
        if (value == "overlapped")
        {
            stdio->flags = static_cast<uv_stdio_flags>(UV_CREATE_PIPE | direction | UV_OVERLAPPED_PIPE);
            auto pipe = new uv_pipe_t{};
            uv_pipe_init(utils::GetUVLoop(env), pipe, 0);
            stdio->data.stream = reinterpret_cast<uv_stream_t *>(pipe);
            return;
        }
        if (value == "ignore")
        {
            stdio->flags = UV_IGNORE;
            return;
        }
        if (value == "inherit")
        {
            stdio->flags = static_cast<uv_stdio_flags>(UV_INHERIT_FD | direction);
            stdio->data.fd = defaultFd;
            return;
        }
        DEFAULT_LOGGER(FATAL) << "Unreachable: invalid pipe options, " << value;
    }
    if (type == napi_number)
    {
        stdio->flags = static_cast<uv_stdio_flags>(UV_INHERIT_FD | direction);
        stdio->data.fd = utils::GetValueInt32(env, option);
        return;
    }
    DEFAULT_LOGGER(FATAL) << "Unreachable: invalid pipe options type, " << type;
}

JsChildProcess::OptionsParser::OptionsParser(napi_env env, napi_value options)
{
    /* options:
    [
        0: string -> file,
        1: string[] -> args,
        2: string[] -> env,
        3: string -> cwd,
        4: boolean -> detach,
        5: string[] | number[] -> stdio(ignore),
        6: number -> uid,
        7: number -> gid,
    ] */
    auto jsOptions = utils::ToVector(env, options);
    strings_.reserve(2);
    strings_.emplace_back(utils::GetValueStringUtf8(env, jsOptions[0]));
    options_.file = strings_.back().data();
    vectors_.resize(2);
    auto &args = ToCharsArray(env, jsOptions[1]);
    options_.args = args.data();
    auto &envs = ToCharsArray(env, jsOptions[2]);
    options_.env = envs.data();

    strings_.emplace_back(utils::GetValueStringUtf8(env, jsOptions[3]));
    options_.cwd = strings_.back().data();
    auto uid = utils::GetValueInt32(env, jsOptions[6]);
    auto gid = utils::GetValueInt32(env, jsOptions[7]);
    int flags = 0;
    if (utils::GetValueBool(env, jsOptions[4]))
        flags |= UV_PROCESS_DETACHED;
    if (uid != getuid())
        flags |= UV_PROCESS_SETUID;
    if (gid != getgid())
        flags |= UV_PROCESS_SETGID;
    options_.flags = flags;
}

JsChildProcess::OptionsParser::~OptionsParser()
{
    for (auto &ptr : memblocks_)
        delete[] ptr;
}

std::vector<char *> &JsChildProcess::OptionsParser::ToCharsArray(napi_env env, napi_value array)
{
    auto args = utils::ToVector<std::string>(env, array, utils::GetValueStringUtf8);
    vectors_.emplace_back(AddStrings(args));
    vectors_.back().emplace_back(nullptr);
    return vectors_.back();
}

std::vector<char *> JsChildProcess::OptionsParser::AddStrings(const std::vector<std::string> &strings)
{
    std::vector<char *> result;
    result.reserve(strings.size());
    size_t size = 0;
    for (auto &el : strings)
    {
        size += el.size() + 1;
    }
    auto mem = new char[size];
    memblocks_.emplace_back(mem);

    size_t idx = 0;
    for (auto &el : strings)
    {
        memcpy(mem + idx, el.c_str(), el.size() + 1);
        result.emplace_back(mem + idx);
        idx += el.size() + 1;
    }
    return result;
}
